<template>
  <article class="page page-localmedia-decrypt">
    <h4 class="text-center">本地HLS视频文件解密</h4>
    <div class="page-body row">
      <div class="col-6">
        <fieldset>
          <legend>参数配置</legend>
          <div class="fieldset-content">
            <div class="main">
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">解密模式</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptMode">
                    <option v-for="(v,i) in decryptModeList" :key="i" :value="v.value">{{v.label}}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">填充方式</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptPadding">
                    <option v-for="(v,i) in decryptPaddingList" :key="i" :value="v.value">{{v.label}}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">秘钥长度</div>
                <div class="row-flex-main">
                  <select class="form-control" v-model="decryptKeySize">
                    <option v-for="(v,i) in decryptKeySizeList" :key="i" :value="v.value">{{v.label}}</option>
                  </select>
                </div>
              </div>
              <div class="row-flex" align="center" v-if="decryptMode!=='ECB'">
                <div class="row-flex-prefix">偏移量IV</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="iv" /></div>
              </div>
              <div class="row-flex" align="center" v-if="(['OFB', 'CFB'].includes(decryptMode))">
                <div class="row-flex-prefix">BlockSize</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="blockSize" /></div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">KEY类型</div>
                <div class="row-flex-main">
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" v-model="keyType" value="str">
                        <label class="form-check-label">字符串</label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" v-model="keyType" value="file">
                        <label class="form-check-label">文件</label>
                    </div>
                    <div class="form-check form-check-inline">
                        <input class="form-check-input" type="radio" v-model="keyType" value="Uint8Array">
                        <label class="form-check-label">Uint8Array</label>
                    </div>
                </div>
              </div>
              <div class="row-flex" align="center" v-if="keyType==='str'">
                <div class="row-flex-prefix"></div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="strKey" :maxlength="decryptKeySize" placeholder="请输入字符串key" /></div>
              </div>
              <div class="row-flex" align="center" v-else-if="keyType==='Uint8Array'">
                <div class="row-flex-prefix"></div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="uint8Key" placeholder="请输入Uint8Array字符串" /></div>
              </div>
              <div class="row-flex" align="center" v-else-if="keyType==='file'">
                <div class="row-flex-prefix"></div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model.trim="fileKey" placeholder="请选择key文件" /></div>
                <div class="row-flex-suffix"><x-button @click="setFileKey">选择</x-button></div>
              </div>
              <div class="row-flex" align="center">
                <div class="row-flex-prefix">保存位置</div>
                <div class="row-flex-main"><input class="form-control" type="text" v-model="toPath" placeholder="文件处理后的存储位置" /></div>
                <div class="row-flex-suffix"><x-button @click="setToPath">选择</x-button></div>
              </div>
            </div>
            <div class="bottom">
              <x-button type="primary" block @click="decrypt" :loading="decryptLoadingCount > 0">解 密</x-button>
            </div>
          </div>
        </fieldset>
      </div>
      <div class="col-6">
        <fieldset>
          <legend>解密</legend>
          <div class="fieldset-content">
            <div class="form-group row-flex" align="center">
              <div class="row-flex-main">待解密文件</div>
              <div class="row-flex-suffix" title="清空" @click="clearList">
                <i class="fa fa-trash-o"></i>
              </div>
            </div>
            <div class="form-group file-list" @drop.prevent="decryptDrop" @dragover.prevent >
              <div class="file-item" v-for="(v,i) in decryptFilesList" :key="v">
                <div class="file-item-text text-ellipsis" :title="v">{{v}}</div>
                <div v-if="(decryptLoadingCount > 0) && (decryptLoadingCount <= i)" class="file-item-icon">
                  <x-loading></x-loading>
                </div>
                <div v-else class="file-item-icon" @click="deleteItem(i)">
                  <i class="fa fa-close"></i>
                </div>
              </div>
              <div v-show="decryptFilesList.length===0" class="nodata">将待解密的文件拖动到此处</div>
            </div>
          </div>
        </fieldset>
      </div>
    </div>
  </article>
</template>

<script>
const fs = global.require('fs')
const path = global.require('path')
const {remote} = global.require('electron')
const {dialog} = remote
const selfWindow= remote.getCurrentWindow()
let CryptoJS = global.CryptoJS || {enc: {}}
CryptoJS.enc.u8array = {
  /**
   * Converts a word array to a Uint8Array.
   * @param {WordArray} wordArray The word array.
   * @return {Uint8Array} The Uint8Array.
   * @static
   * @example
   * var u8arr = CryptoJS.enc.u8array.stringify(wordArray);
   */
  stringify: function (wordArray) {
    // Shortcuts
    var words = wordArray.words
    var sigBytes = wordArray.sigBytes
    // Convert
    var u8 = new Uint8Array(sigBytes)
    for (var i = 0; i < sigBytes; i++) {
      var byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff
      u8[i] = byte
    }
    return u8
  },
  /**
   * Converts a Uint8Array to a word array.
   * @param {string} u8Str The Uint8Array.
   * @return {WordArray} The word array.
   * @static
   * @example
   * var wordArray = CryptoJS.enc.u8array.parse(u8arr);
   */
  parse: function (u8arr) {
    // Shortcut
    var len = u8arr.length
    // Convert
    var words = []
    for (var i = 0; i < len; i++) {
      words[i >>> 2] |= (u8arr[i] & 0xff) << (24 - (i % 4) * 8)
    }
    return CryptoJS.lib.WordArray.create(words, len)
  }
}
// 字符串转字节数组的方法
const stringToBytes = (str) => {
  let ch = []
  let st = []
  let re = []
  for (let i = 0; i < str.length; i++) {
    ch = str.charCodeAt(i) // get char
    st = [] // set up "stack"
    do {
      st.push(ch & 0xFF) // push byte to stack
      ch = ch >> 8 // shift value down by 1 byte
    }
    while (ch)
    // add stack contents to result
    // done because chars have "wrong" endianness
    re = re.concat(st.reverse())
  }
  // return an array of bytes
  return re
}
const base642arraybuffer = (base64) => {
  var binary_string = window.atob(base64)
  var len = binary_string.length
  var bytes = new Uint8Array(len)
  for (var i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i)
  }
  return bytes.buffer
}
const hexToBytes = (hex) => {
  let bytes = []
  for (let c = 0; c < hex.length; c += 2) {
    bytes.push(parseInt(hex.substr(c, 2), 16))
  }
  return bytes
}

export default {
  name: 'localmedia-decrypt',
  data () {
    return {
      decryptMode: 'CBC',
      decryptPadding: 'Pkcs7',
      decryptKeySize: 16,
      decryptModeList: [
        {label: 'CBC', value: 'CBC'},
        {label: 'ECB', value: 'ECB'},
        {label: 'CTR', value: 'CTR'},
        {label: 'CTRGladman', value: 'CTRGladman'},
        {label: 'OFB', value: 'OFB'},
        {label: 'CFB', value: 'CFB'},
      ],
      decryptPaddingList: [
        {label: 'Pkcs7', value: 'Pkcs7'},
        {label: 'ZeroPadding', value: 'ZeroPadding'},
        {label: 'Iso10126', value: 'Iso10126'},
        {label: 'Iso97971', value: 'Iso97971'},
        {label: 'AnsiX923', value: 'AnsiX923'},
        {label: 'NoPadding', value: 'NoPadding'},
      ],
      decryptKeySizeList: [
        {label: '128bits', value: 16},
        {label: '192bits', value: 24},
        {label: '256bits', value: 32},
      ],
      iv: '',
      blockSize: '',
      key: '',
      keyType: 'str',
      strKey: '',
      uint8Key: '',
      fileKey: '',
      toPath: this.$sharedObject('desktopPath'),
      taskList: [],
      dataList: [],
      decryptFilesList: [],
      decryptLoadingCount: 0
    }
  },
  mounted () {
  },
  methods: {
    // 设置保存位置
    setToPath () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择目录',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openDirectory']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.toPath = filePaths[0]
      }
    },
    // 设置Key文件
    setFileKey () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择文件',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openFile']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.fileKey = filePaths[0]
      }
    },
    // 拖拽添加待解密文件
    decryptDrop (e) {
      const fileList = e.dataTransfer.files
      if (!fileList) return
      for (let i = 0; i < fileList.length; i++) {
        const file = fileList[i]
        let fileStat = fs.statSync(file.path)
        if (fileStat.isDirectory()) {
          continue
        }
        this.decryptFilesList.push(file.path)
      }
      if (this.decryptFilesList.length === 0) {
        this.$toast('请选择待解密的文件！')
      }
    },
    // 删除条目
    deleteItem (index) {
        this.decryptFilesList.splice(index, 1)
    },
    clearList () {
        this.decryptFilesList = []
        this.decryptLoadingCount = 0
    },
    // 解密
    decrypt () {
        if (this.decryptFilesList.length === 0) {
            return this.$toast('请选择待解密的文件！')
        }
        // if (this.decryptMode === 'ECB') { this.iv = '' }
        this.getKey().then(() => {
            this.decryptLoadingCount = this.decryptFilesList.length
            this.decryptFilesList.forEach((v, i) => {
                console.log(i)
                this.taskList.push(
                    this.decryptItem(v).then((data) => {
                        let _arr = path.basename(v).split('.')
                        let _ext = ''
                        if (_arr.length > 1) {_ext = _arr.pop()}
                        let fileName = _arr.join('.') + '_decrypt'
                        if (_ext) {fileName += `.${_ext}`}
                        let filePath = path.resolve(this.toPath, fileName)
                        return this.$sharedObject('jstoolsNode').writeFile(filePath, Buffer.from(data))
                    }).finally(() => {
                        this.decryptLoadingCount--
                    })
                )
                Promise.all(this.taskList).finally(() => {
                    this.$toast('处理完毕')
                    this.taskList = []
                })
            })
        }).catch(err => {
            this.$toast(err)
        })
    },
    // 解密Item
    decryptItem (filePath) {
        return new Promise((resolve, reject) => {
            if (!fs.existsSync(filePath)) {
                reject('文件不存在')
            }
            fs.readFile(filePath, (err, data) => {
                if (err || !data) reject('读取文件错误')
                let result = this.decryptByAes(data)
                resolve(result)
            })
        })
    },
    // 获取key
    getKey () {
        return new Promise((resolve, reject) => {
            if (this.keyType === 'str') {
                if (this.strKey.length > parseInt(this.decryptKeySize)) {
                  this.strKey = this.strKey.substring(0, parseInt(this.decryptKeySize))
                }
                this.key = this.strKey
                if (!this.key) { reject('KEY不存在') } else {resolve()}
            } else if (this.keyType === 'file') {
                if (!fs.existsSync(this.fileKey)) { reject('文件不存在') }
                fs.readFile(this.fileKey, (err, data) => {
                    if (err || !data) reject('读取文件错误')
                    let keyBuffer = new Uint8Array(data.buffer)
                    keyBuffer = keyBuffer.slice(0, 16)
                    this.key = CryptoJS.enc.u8array.parse(keyBuffer)
                    console.log('keyBuffer:', keyBuffer)
                    console.log('key:', this.key)
                    resolve()
                })
            } else if (this.keyType === 'Uint8Array') {
              this.key = new Uint8Array(this.$jstools.jsonParse(this.uint8Key, []))
              if (!this.key) { reject('KEY不存在') } else { resolve() }
            }
        })
    },
    // AES解密
    decryptByAes (buffer) {
        let key = this.key || ''
        let iv = this.iv || ''
        // 处理key和iv的格式
        let keyWA = key
        if (typeof key === 'string') {
            // 密钥转字节数组(16位)
            let keyBy = stringToBytes(key)
            // 字节数组转Uint8Array
            let keyBv = new Uint8Array(keyBy)
            // Uint8Array转WordArray
            keyWA = CryptoJS.enc.u8array.parse(keyBv)
        }
        let ivWA = iv
        if (typeof iv === 'string') {
            let ivBy = hexToBytes(iv)
            let ivBv = new Uint8Array(ivBy)
            ivWA = CryptoJS.enc.u8array.parse(ivBv)
        }
        let view = new Uint8Array(buffer)
        // 将Uint8Array 转成 WordArray
        let contentWA = CryptoJS.enc.u8array.parse(view)
        // base64字符串
        let dcBase64String = contentWA.toString(CryptoJS.enc.Base64)
        // 解密
        let decryptedData = CryptoJS.AES.decrypt(dcBase64String, keyWA, {
            iv: ivWA,
            mode: CryptoJS.mode[this.decryptMode],
            padding: CryptoJS.pad[this.decryptPadding]
        })
        // 把解密后的对象再转为base64编码,这步是关键,跟解密文字不同
        let d64 = decryptedData.toString(CryptoJS.enc.Base64)
        return base642arraybuffer(d64)
    },
  }
}
</script>

<style scoped lang="less">
@import "~@/assets/css/_mixins.less";
@import "~@/assets/css/_color.less";
.page-localmedia-decrypt {
  display: flex;
  flex-direction: column;
  .row-flex {
    & + .row-flex {
      margin-top: 1em;
    }
    .row-flex-prefix {
      min-width: 5em;
      text-align: right;
    }
    .row-flex-main {
      text-align: left;
    }
}
	.page-body {
		height: 0;
		flex-grow: 1;
		overflow: hidden;
        position: relative;
        fieldset {
            height: 100%;
        }
        .fieldset-content {
            height: calc(100% - 2em);
            display: flex;
            flex-direction: column;
            .main {
                height: 0;
                flex-grow: 1;
                overflow: hidden auto;
            }
            .bottom {
                .x-button {
                    margin-top: .5em;
                }
            }
            .file-list {
                height: 0;
                flex-grow: 1;
                padding: .5em;
                border-radius: .5em;
                background-color: #fff;
                box-shadow: 0 0 .5em @bgGrey;
                overflow: hidden auto;
                position: relative;
                .file-item {
                    height: 2em;
                    display: flex;
                    align-items: center;
                    .file-item-text {
                    width: 0;
                    flex-grow: 1;
                    }
                    .file-item-icon {
                    padding: 0 .5em;
                    cursor: pointer;
                    }
                }
            }
        }
    }
}
</style>
